前言

作为 laravel 极其重要的一部分,route 功能贯穿着整个网络请求,是 request 生命周期的主干。本文主要讲述 route 服务的注册与启动、路由的属性注册。本篇内容相对简单,更多的是框架添加路由的整体设计流程。

route 服务的注册

laravel 在接受到请求后,先进行了服务容器与 http 核心的初始化,再进行了请求 request 的构造与分发。

route 服务的注册—— RoutingServiceProvider 发生在服务容器 container 的初始化上;

route 服务的启动与加载—— RouteServiceProvider 发生在 request 的分发上。

route 服务的注册——RoutingServiceProvider

所有需要 laravel 服务的请求都会加载入口文件 index.php:

  1. require __DIR__.'/../bootstrap/autoload.php';
  2. $app = require_once __DIR__.'/../bootstrap/app.php';

第一句我们在之前的博客提过,是实现 PSR0PSR4标准自动加载的功能模块,第二句就是今天说的 container 的初始化:

  1. $app = new Illuminate\Foundation\Application(
  2. realpath(__DIR__.'/../')
  3. );

Application :

  1. namespace Illuminate\Foundation;
  2. class Application extends Container implements ApplicationContract, HttpKernelInterface
  3. {
  4. public function __construct($basePath = null)
  5. {
  6. if ($basePath) {
  7. $this->setBasePath($basePath);
  8. }
  9. $this->registerBaseBindings();
  10. $this->registerBaseServiceProviders();
  11. $this->registerCoreContainerAliases();
  12. }
  13. }

路由服务的注册就在 registerBaseServiceProviders() 这个函数中:

  1. protected function registerBaseServiceProviders()
  2. {
  3. $this->register(new EventServiceProvider($this));
  4. $this->register(new LogServiceProvider($this));
  5. $this->register(new RoutingServiceProvider($this));
  6. }

RoutingServiceProvider :

  1. namespace Illuminate\Routing;
  2. class RoutingServiceProvider extends ServiceProvider
  3. {
  4. public function register()
  5. {
  6. $this->registerRouter();
  7. ...
  8. }
  9. protected function registerRouter()
  10. {
  11. $this->app->singleton('router', function ($app) {
  12. return new Router($app['events'], $app);
  13. });
  14. }
  15. ...
  16. }

可以看到,RoutingServiceProvider 做的事情比较简单,就是向服务容易中注册 router

route 服务的启动与加载——RouteServiceProvider

laravel 在初始化 Application 后,就要进行 http/Kernel 的构造:

  1. $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
  2. $response = $kernel->handle(
  3. $request = Illuminate\Http\Request::capture()
  4. );

初始化结束后,就会调用 handle 函数,这个函数用于 laravel 各个功能服务的注册启动,还有 request 的分发:

  1. public function handle($request)
  2. {
  3. try {
  4. $request->enableHttpMethodParameterOverride();
  5. $response = $this->sendRequestThroughRouter($request);
  6. }
  7. return $response;
  8. }
  9. protected function sendRequestThroughRouter($request)
  10. {
  11. $this->app->instance('request', $request);
  12. Facade::clearResolvedInstance('request');
  13. $this->bootstrap();//各种服务的注册与启动
  14. return (new Pipeline($this->app))//请求的分发
  15. ->send($request)
  16. ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
  17. ->then($this->dispatchToRouter());
  18. }

路由服务的启动与加载就在其中一个函数中 bootstrap,这个函数用于各种服务的注册与启动,比较复杂,我们有机会在以后单独来说。

总之,这个函数会调用 RouteServiceProvider 这个类的两个函数: 注册——register、启动——boot

由于 route 的注册工作由之前 RoutingServiceProvider 完成,所以 RouteServiceProviderregister 是空的,这里它只负责路由的启动与加载工作,我们主要看 boot

  1. namespace Illuminate\Foundation\Support\Providers;
  2. class RouteServiceProvider extends ServiceProvider
  3. {
  4. public function register()
  5. {
  6. //
  7. }
  8. public function boot()
  9. {
  10. $this->setRootControllerNamespace();
  11. if ($this->app->routesAreCached()) {
  12. $this->loadCachedRoutes();
  13. } else {
  14. $this->loadRoutes();
  15. $this->app->booted(function () {
  16. $this->app['router']->getRoutes()->refreshNameLookups();
  17. $this->app['router']->getRoutes()->refreshActionLookups();
  18. });
  19. }
  20. }
  21. protected function loadCachedRoutes()
  22. {
  23. $this->app->booted(function () {
  24. require $this->app->getCachedRoutesPath();
  25. });
  26. }
  27. protected function loadRoutes()
  28. {
  29. if (method_exists($this, 'map')) {
  30. $this->app->call([$this, 'map']);
  31. }
  32. }
  33. }
  34. class Application extends Container implements ApplicationContract, HttpKernelInterface
  35. {
  36. public function routesAreCached()
  37. {
  38. return $this['files']->exists($this->getCachedRoutesPath());
  39. }
  40. public function getCachedRoutesPath()
  41. {
  42. return $this->bootstrapPath().'/cache/routes.php';
  43. }
  44. }

boot 中可以看出,laravel 首先去寻找路由的缓存文件,没有缓存文件再去进行加载路由。缓存文件一般在 bootstrap/cache/routes.php 文件中。

加载路由主要调用 map 函数,这个函数一般在 App\Providers\RouteServiceProvider 这个类中,这个类继承上面的 Illuminate\Foundation\Support\Providers\RouteServiceProvider:

  1. use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
  2. class RouteServiceProvider extends ServiceProvider
  3. {
  4. public function map()
  5. {
  6. $this->mapApiRoutes();
  7. $this->mapWebRoutes();
  8. //
  9. }
  10. protected function mapWebRoutes()
  11. {
  12. Route::middleware('web')
  13. ->namespace($this->namespace)
  14. ->group(base_path('routes/web.php'));
  15. }
  16. protected function mapApiRoutes()
  17. {
  18. Route::prefix('api')
  19. ->middleware('api')
  20. ->namespace($this->namespace)
  21. ->group(base_path('routes/api.php'));
  22. }
  23. }

laravle 将路由分为两个大组:apiweb。这两个部分的路由分别写在两个文件中:routes/web.phproutes/api.php

路由的加载

所谓的路由加载,就是将定义路由时添加的属性,例如 ‘name’、’domain’、’scheme’ 等等保存起来,以待后用。

  • laravel 定义路由的属性的方法很灵活,可以定义在路由群组前,例如:
  1. Route::domain('route.domain.name')
  2. ->group(function() {
  3. Route::get('foo','controller@method');
  4. })
  • 可以定义在路由群组中,例如:
  1. Route::group('domain' => 'group.domain.name',function() {
  2. Route::get('foo','controller@method');
  3. })
  • 可以定义在 method 的前面,例如:
  1. Route::domain('route.domain.name')
  2. ->get('foo','controller@method');
  • 可以定义在 method 中,例如:
  1. Route::get('foo',['domain' => 'route.domain.name','use' => 'controller@method']);
  • 还可以定义在 method 后,例如:
  1. Route::get('{one}', 'use' => 'controller@method')
  2. ->where('one', '(.+)');

事实上,路由的加载功能主要有三个类负责: Illuminate\Routing\RouterIlluminate\Routing\RouteIlluminate\Routing\RouteRegistrar

Router 在整个路由功能中都是起着中枢的作用, RouteRegistrar 主要负责位于 groupmethod 这些函数之前的属性注册,例如上面的第一种和第三种,route 主要负责位于 groupmethod 这些函数之后的属性注册,例如第五种。

RouteRegistrar 路由加载

属性注册

当我们想要在 Route 后面直接利用 domain()name() 等函数来为路由注册属性的时候,我们实际调用的是 router 的魔术方法 __call():

  1. namespace Illuminate\Routing;
  2. class Router implements RegistrarContract, BindingRegistrar
  3. {
  4. public function __call($method, $parameters)
  5. {
  6. if (static::hasMacro($method)) {
  7. return $this->macroCall($method, $parameters);
  8. }
  9. return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
  10. }
  11. }

在类 RouteRegistrar 中:

  1. class RouteRegistrar
  2. {
  3. protected $allowedAttributes = [
  4. 'as', 'domain', 'middleware', 'name', 'namespace', 'prefix',
  5. ];
  6. public function attribute($key, $value)
  7. {
  8. if (! in_array($key, $this->allowedAttributes)) {
  9. throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
  10. }
  11. $this->attributes[array_get($this->aliases, $key, $key)] = $value;
  12. return $this;
  13. }
  14. }

添加路由

注册属性之后,创建路由的时候,可以仅仅提供 uri,可以提供 uri 与 闭包,可以提供 uri 与 控制器,可以提供 uri 与数组:

  1. Route::as('Foo')
  2. ->namespace('Namespace\\Example\\')
  3. ->get('foo/bar');//仅仅 uri
  4. Route::as('Foo')
  5. ->namespace('Namespace\\Example\\')
  6. ->get('foo/bar', function () {
  7. }); //uri 与闭包
  8. Route::as('Foo')
  9. ->namespace('Namespace\\Example\\')
  10. ->get('foo/bar', 'controller@method');//uri 与控制器
  11. Route::as('Foo')
  12. ->namespace('Namespace\\Example\\')
  13. ->get('foo/bar', ['as'=> 'foo','use' =>'controller@method']);//uri 与数组

利用 getpost 等方法创建新的路由时,会调用类 RouteRegistrar 中的魔术方法 __call()

  1. class RouteRegistrar
  2. {
  3. protected $passthru = [
  4. 'get', 'post', 'put', 'patch', 'delete', 'options', 'any',
  5. ];
  6. public function __call($method, $parameters)
  7. {
  8. if (in_array($method, $this->passthru)) {
  9. return $this->registerRoute($method, ...$parameters);
  10. }
  11. if (in_array($method, $this->allowedAttributes)) {
  12. return $this->attribute($method, $parameters[0]);
  13. }
  14. throw new BadMethodCallException("Method [{$method}] does not exist.");
  15. }
  16. protected function registerRoute($method, $uri, $action = null)
  17. {
  18. if (! is_array($action)) {
  19. $action = array_merge($this->attributes, $action ? ['uses' => $action] : []);
  20. }
  21. return $this->router->{$method}($uri, $this->compileAction($action));
  22. }
  23. protected function compileAction($action)
  24. {
  25. if (is_null($action)) {
  26. return $this->attributes;
  27. }
  28. if (is_string($action) || $action instanceof Closure) {
  29. $action = ['uses' => $action];
  30. }
  31. return array_merge($this->attributes, $action);
  32. }
  33. }

也就是说,RouteRegistrar 在这里会为闭包或控制器等所有非数组的 action 添加 use 键,然后才会去 router 中创建路由。

添加路由群组

注册属性之后,还可以创建路由群组,但是这时路由群组不允许添加属性 action

  1. class RouteRegistrar
  2. {
  3. public function group($callback)
  4. {
  5. $this->router->group($this->attributes, $callback);
  6. }
  7. }

Router 路由群组加载

路由群组的功能可以不断叠加递归,因此每次调用 group ,都要用新路由群组的属性与旧路由群组属性合并,以待新的路由去继承。 group 参数可以是闭包函数,也可以是包含定义路由的文件路径。

  1. public function group(array $attributes, $routes)
  2. {
  3. $this->updateGroupStack($attributes);
  4. $this->loadRoutes($routes);
  5. array_pop($this->groupStack);
  6. }
  7. protected function updateGroupStack(array $attributes)
  8. {
  9. if (! empty($this->groupStack)) {
  10. $attributes = RouteGroup::merge($attributes, end($this->groupStack));
  11. }
  12. $this->groupStack[] = $attributes;
  13. }
  14. protected function loadRoutes($routes)
  15. {
  16. if ($routes instanceof Closure) {
  17. $routes($this);
  18. } else {
  19. $router = $this;
  20. require $routes;
  21. }
  22. }

关于路由群组属性的合并,

  • prefixasnamespace 这几个属性会连接在一起,例如 prefix1/prefix2/prefix3
  • where 属性数组相同的会被替换,不同的会被合并。
  • domain 属性会被替换。
  • 其他属性,例如 middleware 数组会直接被合并,即使存在相同的元素。
  1. class RouteGroup
  2. {
  3. public static function merge($new, $old)
  4. {
  5. if (isset($new['domain'])) {
  6. unset($old['domain']);
  7. }
  8. $new = array_merge(static::formatAs($new, $old), [
  9. 'namespace' => static::formatNamespace($new, $old),
  10. 'prefix' => static::formatPrefix($new, $old),
  11. 'where' => static::formatWhere($new, $old),
  12. ]);
  13. return array_merge_recursive(Arr::except(
  14. $old, ['namespace', 'prefix', 'where', 'as']
  15. ), $new);
  16. }
  17. }

Router 路由加载

添加路由需要很多步骤,需要将路由本身的属性和路由群组的属性相结合。

  1. public function get($uri, $action = null)
  2. {
  3. return $this->addRoute(['GET', 'HEAD'], $uri, $action);
  4. }
  5. protected function addRoute($methods, $uri, $action)
  6. {
  7. return $this->routes->add($this->createRoute($methods, $uri, $action));
  8. }
  9. protected function createRoute($methods, $uri, $action)
  10. {
  11. if ($this->actionReferencesController($action)) {
  12. $action = $this->convertToControllerAction($action);
  13. }
  14. $route = $this->newRoute(
  15. $methods, $this->prefix($uri), $action
  16. );
  17. if ($this->hasGroupStack()) {
  18. $this->mergeGroupAttributesIntoRoute($route);
  19. }
  20. $this->addWhereClausesToRoute($route);
  21. return $route;
  22. }

从上面来看,添加一个新的路由需要:

  • 给路由的控制器添加 groupnamespace
  • 给路由的 uri 添加 groupprefix 前缀
  • 创建新的路由
  • 更新路由的属性信息
  • 为路由添加 router-pattern 正则约束
  • 路由添加到 RouteCollection

控制器 namespace

路由控制器的命名空间一般不用特别指定,默认值是 \App\Http\Controllers,每次创建新的路由,都要将默认的命名空间添加到控制器中去:

  1. protected function actionReferencesController($action)
  2. {
  3. if (! $action instanceof Closure) {
  4. return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
  5. }
  6. return false;
  7. }
  8. protected function convertToControllerAction($action)
  9. {
  10. if (is_string($action)) {
  11. $action = ['uses' => $action];
  12. }
  13. if (! empty($this->groupStack)) {
  14. $action['uses'] = $this->prependGroupNamespace($action['uses']);
  15. }
  16. $action['controller'] = $action['uses'];
  17. return $action;
  18. }
  19. protected function prependGroupNamespace($class)
  20. {
  21. $group = end($this->groupStack);
  22. return isset($group['namespace']) && strpos($class, '\\') !== 0
  23. ? $group['namespace'].'\\'.$class : $class;
  24. }

uri 前缀

在创建新的路由前,需要将路由群组的 prefix 添加到路由的 uri 中:

  1. protected function prefix($uri)
  2. {
  3. return trim(trim($this->getLastGroupPrefix(), '/').'/'.trim($uri, '/'), '/') ?: '/';
  4. }
  5. public function getLastGroupPrefix()
  6. {
  7. if (! empty($this->groupStack)) {
  8. $last = end($this->groupStack);
  9. return isset($last['prefix']) ? $last['prefix'] : '';
  10. }
  11. return '';
  12. }

创建新的路由

路由的创建需要 Route 类:

  1. protected function newRoute($methods, $uri, $action)
  2. {
  3. return (new Route($methods, $uri, $action))
  4. ->setRouter($this)
  5. ->setContainer($this->container);
  6. }

关于 Router 类添加新的路由我们在下一部分详细说。

更新路由属性信息

创建新的路由之后,需要将路由本身的属性 action 与路由群组的属性结合在一起:

  1. public function hasGroupStack()
  2. {
  3. return ! empty($this->groupStack);
  4. }
  5. protected function mergeGroupAttributesIntoRoute($route)
  6. {
  7. $route->setAction($this->mergeWithLastGroup($route->getAction()));
  8. }

添加全局正则约束到路由

上一篇文章我们说过,我们可以为路由通过 pattern 方法添加全局的参数正则约束,所有每次添加新的路由都要将这个全局正则约束添加到路由中:

  1. public function pattern($key, $pattern)
  2. {
  3. $this->patterns[$key] = $pattern;
  4. }
  5. protected function addWhereClausesToRoute($route)
  6. {
  7. $route->where(array_merge(
  8. $this->patterns, isset($route->getAction()['where']) ? $route->getAction()['where'] : []
  9. ));
  10. return $route;
  11. }

Route 路由加载

前面说过,路由的创建是由 Route 这个类完成的:

  1. public function __construct($methods, $uri, $action)
  2. {
  3. $this->uri = $uri;
  4. $this->methods = (array) $methods;
  5. $this->action = $this->parseAction($action);
  6. if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
  7. $this->methods[] = 'HEAD';
  8. }
  9. if (isset($this->action['prefix'])) {
  10. $this->prefix($this->action['prefix']);
  11. }
  12. }

由此可以看出,路由的创建主要是路由的各个属性的初始化,其中值得注意的有两个: actionprefix

action 解析

  1. protected function parseAction($action)
  2. {
  3. return RouteAction::parse($this->uri, $action);
  4. }

我们可以看出,添加新的路由时, action 属性需要利用 RouteAction 类:

  1. class RouteAction
  2. {
  3. public static function parse($uri, $action)
  4. {
  5. if (is_null($action)) {
  6. return static::missingAction($uri);
  7. }
  8. if (is_callable($action)) {
  9. return ['uses' => $action];
  10. }
  11. elseif (! isset($action['uses'])) {
  12. $action['uses'] = static::findCallable($action);
  13. }
  14. if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
  15. $action['uses'] = static::makeInvokable($action['uses']);
  16. }
  17. return $action;
  18. }
  19. protected static function findCallable(array $action)
  20. {
  21. return Arr::first($action, function ($value, $key) {
  22. return is_callable($value) && is_numeric($key);
  23. });
  24. }
  25. protected static function makeInvokable($action)
  26. {
  27. if (! method_exists($action, '__invoke')) {
  28. throw new UnexpectedValueException("Invalid route action: [{$action}].");
  29. }
  30. return $action.'@__invoke';
  31. }
  32. }

前面的博客我们说过,创建路由的时候,除了为路由分配控制器之外,还可以为路由分配闭包函数,还有类函数,例如之前说的单动作控制器:

  1. $router->get('foo/bar2', [‘domain => 'www.example.com', 'Illuminate\Tests\Routing\ActionStub']);
  2. class ActionStub
  3. {
  4. public function __invoke()
  5. {
  6. return 'hello';
  7. }
  8. }

因此,解析 action 主要做两件事:

  • 为闭包函数添加 use 键。对于此时没有 use 键的路由,由于之前在 Router 中已经为控制器添加 use 键,因此这时没有 use 键的,必然是闭包函数,在这里直接或者在 action 中寻找闭包函数后,为闭包函数添加 use 键。

  • 单动作控制器添加 __invoke。对于单动作控制器来说,此时已经和控制器一样拥有 ‘use’ 键,但是并没有 @ 符号,此时就会调用 makeInvokable 函数来将 __invoke 添加到后面。

prefix 前缀

路由自身也有 prefix 属性,而且这个属性要加在其他 prefix 的最前面,作为路由的 uri

  1. public function prefix($prefix)
  2. {
  3. $uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
  4. $this->uri = trim($uri, '/');
  5. return $this;
  6. }

Route 路由属性加载

除了 RouteRegistrar 之外,Route 也可以为路由添加属性:

prefix 前缀

  1. public function prefix($prefix)
  2. {
  3. $uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
  4. $this->uri = trim($uri, '/');
  5. return $this;
  6. }

where 正则约束

  1. public function where($name, $expression = null)
  2. {
  3. foreach ($this->parseWhere($name, $expression) as $name => $expression) {
  4. $this->wheres[$name] = $expression;
  5. }
  6. return $this;
  7. }
  8. protected function parseWhere($name, $expression)
  9. {
  10. return is_array($name) ? $name : [$name => $expression];
  11. }

middleware 中间件

  1. public function middleware($middleware = null)
  2. {
  3. if (is_null($middleware)) {
  4. return (array) Arr::get($this->action, 'middleware', []);
  5. }
  6. if (is_string($middleware)) {
  7. $middleware = func_get_args();
  8. }
  9. $this->action['middleware'] = array_merge(
  10. (array) Arr::get($this->action, 'middleware', []), $middleware
  11. );
  12. return $this;
  13. }

uses 控制器

  1. public function uses($action)
  2. {
  3. $action = is_string($action) ? $this->addGroupNamespaceToStringUses($action) : $action;
  4. return $this->setAction(array_merge($this->action, $this->parseAction([
  5. 'uses' => $action,
  6. 'controller' => $action,
  7. ])));
  8. }

name 命名

  1. public function name($name)
  2. {
  3. $this->action['as'] = isset($this->action['as']) ? $this->action['as'].$name : $name;
  4. return $this;
  5. }

RouteCollection 添加路由

在上面的部分,我们看到添加路由的代码:

  1. protected function addRoute($methods, $uri, $action)
  2. {
  3. return $this->routes->add($this->createRoute($methods, $uri, $action));
  4. }

新创建的路由会加入到 RouteCollection 中,会更新类中的 routesallRoutesnameListactionList

  1. public function add(Route $route)
  2. {
  3. $this->addToCollections($route);
  4. $this->addLookups($route);
  5. return $route;
  6. }
  7. protected function addToCollections($route)
  8. {
  9. $domainAndUri = $route->domain().$route->uri();
  10. foreach ($route->methods() as $method) {
  11. $this->routes[$method][$domainAndUri] = $route;
  12. }
  13. $this->allRoutes[$method.$domainAndUri] = $route;
  14. }
  15. protected function addLookups($route)
  16. {
  17. $action = $route->getAction();
  18. if (isset($action['as'])) {
  19. $this->nameList[$action['as']] = $route;
  20. }
  21. if (isset($action['controller'])) {
  22. $this->addToActionList($action, $route);
  23. }
  24. }
  25. protected function addToActionList($action, $route)
  26. {
  27. $this->actionList[trim($action['controller'], '\\')] = $route;
  28. }

我们在上面路由的注册启动章节说道,路由的启动是 namespace Illuminate\Foundation\Support\Providers\RouteServiceProvider 完成的,调用的是 boot 函数:

  1. public function boot()
  2. {
  3. $this->setRootControllerNamespace();
  4. if ($this->app->routesAreCached()) {
  5. $this->loadCachedRoutes();
  6. } else {
  7. $this->loadRoutes();
  8. $this->app->booted(function () {
  9. $this->app['router']->getRoutes()->refreshNameLookups();
  10. });
  11. }
  12. }

在最后一句,程序将会在所有服务都启动后运行 refreshNameLookups 函数,把所有的 name 属性加载到 RouteCollection 中:

  1. public function refreshNameLookups()
  2. {
  3. $this->nameList = [];
  4. foreach ($this->allRoutes as $route) {
  5. if ($route->getName()) {
  6. $this->nameList[$route->getName()] = $route;
  7. }
  8. }
  9. }

测试样例如下:

  1. public function testRouteCollectionCanRefreshNameLookups()
  2. {
  3. $routeIndex = new Route('GET', 'foo/index', [
  4. 'uses' => 'FooController@index',
  5. ]);
  6. $this->assertNull($routeIndex->getName());
  7. $this->routeCollection->add($routeIndex)->name('route_name');
  8. $this->assertNull($this->routeCollection->getByName('route_name'));
  9. $this->routeCollection->refreshNameLookups();
  10. $this->assertEquals($routeIndex, $this->routeCollection->getByName('route_name'));
  11. }